﻿//  Copyright © 2009-2010 by Rhy A. Mednick
//  All rights reserved.
//  http://rhyduino.codeplex.com
//  
//  Redistribution and use in source and binary forms, with or without modification, 
//  are permitted provided that the following conditions are met:
//  
//  * Redistributions of source code must retain the above copyright notice, this list 
//    of conditions and the following disclaimer.
//  
//  * Redistributions in binary form must reproduce the above copyright notice, this 
//    list of conditions and the following disclaimer in the documentation and/or other 
//    materials provided with the distribution.
//  
//  * Neither the name of Rhy A. Mednick nor the names of its contributors may be used 
//    to endorse or promote products derived from this software without specific prior 
//    written permission.
//  
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
//  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
//  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
//  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
//  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Rhyduino.MockArduino.Properties;

namespace Rhyduino.MockArduino
{
    internal class VirtualComPort : IDisposable
    {
        #region Constructor(s)

        public VirtualComPort()
        {
            var token = _cancellationTokenSource.Token;

            PopulateResponseDictionary();

            using (var listenTask = Task.Factory.StartNew(() => ListenForClients(token), token))
            {
                _listenTask = listenTask;
                Console.WriteLine("When done, press <enter> to close emulator.");
                Console.ReadLine();
                _cancellationTokenSource.Cancel();
                listenTask.Wait();
                _listenTask = null;
            }
        }

        #endregion

        #region Private Methods

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void HandleClientComm(TcpClient tcpClient, CancellationToken token)
        {
            var clientStream = tcpClient.GetStream();
            var message = new byte[4096];
            var cancelled = false;
            while (!cancelled)
            {
                clientStream.BeginRead(message, 0, 4096,
                                       a =>
                                           {
                                               try
                                               {
                                                   var bytesRead = clientStream.EndRead(a);
                                                   var package = message.Trim();
                                                   foreach (var b in package)
                                                   {
                                                       _serialQueue.Enqueue(b);
                                                   }
                                                   Console.WriteLine("{0}", package.ToHexString());
                                                   ParseMessage(clientStream);
                                                   if (bytesRead == 0)
                                                   {
                                                       // Client has disconnected.
                                                       cancelled = true;
                                                   }
                                               }
                                               catch
                                               {
                                                   // socket error
                                                   cancelled = true;
                                               }
                                           }, null);
                if (cancelled)
                {
                    continue;
                }
                // check every quarter-second to see if we've been cancelled
                Thread.Sleep(250);
                cancelled = token.IsCancellationRequested;
            }
            clientStream.Close();
            tcpClient.Close();
        }

        private static byte[] HexByteStringToByteArray(string request)
        {
            var values = request.Split(new[] {' '});
            var result = new byte[values.Length];
            for (var i = 0; i < values.Length; i++)
            {
                result[i] = (byte) Convert.ToInt16(values[i].Trim(), 16);
            }
            return result;
        }

        private static bool IsCommandByte(byte current)
        {
            // A byte from a message is considered a command if the highest 
            // bit is set. This is why data messages are formatted as 
            // 7-bit bytes.
            return ((current & 0x80) == 0x80);
        }

        private void ListenForClients(CancellationToken token)
        {
            var tcpListener = new TcpListener(IPAddress.Any, Settings.Default.ListenPort);
            var cancelled = false;

            tcpListener.Start();

            while (!cancelled)
            {
                //blocks until a client has connected to the server
                tcpListener.BeginAcceptTcpClient(a =>
                                                     {
                                                         var client = tcpListener.EndAcceptTcpClient(a);
                                                         using (
                                                             var listenTask =
                                                                 Task.Factory.StartNew(
                                                                     () => HandleClientComm(client, token), token))
                                                         {
                                                             listenTask.Wait();
                                                         }
                                                     }, null);

                // check every quarter-second to see if we've been cancelled
                Thread.Sleep(250);
                cancelled = token.IsCancellationRequested;
            }
        }

        private void ParseMessage(NetworkStream clientStream)
        {
            _inSysex = false;

            while (_serialQueue.Count > 0)
            {
                var current = _serialQueue.Dequeue();

                switch (current)
                {
                    case (byte) ResponseMessageType.SysexStart:
                        _inSysex = true;
                        _local.Add(current);
                        break;
                    case (byte) ResponseMessageType.SysexEnd:
                        _inSysex = false;
                        // process the local queue
                        _local.Add(current);
                        RespondToMessage(_local.ToArray(), clientStream);
                        _local.Clear();
                        break;
                    default:
                        // If we're in a sysex block, just ignore the data
                        if (_inSysex)
                        {
                            _local.Add(current);
                        }
                        else
                        {
                            if (IsCommandByte(current) && _local.Count > 0)
                            {
                                // If it's a command byte and the cache has data, raise the event and flush the cache
                                RespondToMessage(_local.ToArray(), clientStream);
                                _local.Clear();
                            }
                            _local.Add(current);
                        }
                        break;
                }
            }
        }

        private void PopulateResponseDictionary()
        {
            var sourceFile = Settings.Default.ResponseDictionary;
            if (String.IsNullOrEmpty(sourceFile))
            {
                throw new InvalidDataException("Name provided for dictionary source file was empty.");
            }
            if (!File.Exists(sourceFile))
            {
                throw new InvalidDataException(
                    String.Format(CultureInfo.CurrentCulture, @"Response dictionary source file was not found. {0}\{1}",
                                  Environment.CurrentDirectory, sourceFile));
            }

            var lines = File.ReadAllLines(sourceFile);
            foreach (var line in lines)
            {
                //skip empty lines
                if (string.IsNullOrEmpty(line))
                {
                    continue;
                }

                //skip comments
                if (line.StartsWith("#", true, CultureInfo.CurrentCulture))
                {
                    continue;
                }

                var regex = new Regex(@"^\[(?<Request>.+)\]\s*\{(?<Response>.+)\}$", RegexOptions.CultureInvariant);
                if (regex.IsMatch(line))
                {
                    // We have a line that matches our regular expression, so pull the data out and add it to the response dictionary
                    var m = regex.Match(line);
                    var request = m.Groups["Request"].Value;
                    var response = m.Groups["Response"].Value;

                    var requestData = HexByteStringToByteArray(request);
                    var responseData = HexByteStringToByteArray(response);
                    _responseDictionary.Add(requestData.BytesToHashCode(), responseData);
                }
            }
        }

        private void RespondToMessage(byte[] message, NetworkStream clientStream)
        {
            // Look up the message in our hash table.
            var key = message.BytesToHashCode();
            if (_responseDictionary.ContainsKey(key))
            {
                var response = _responseDictionary[key];
                // now send it down the response port
                clientStream.Write(response, 0, response.Length);
            }
            else
            {
                Console.WriteLine("Ignored message.");
            }
        }

        #endregion

        #region Implementation of IDisposable interface

        #region Public Methods

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region Private Fields

        private bool _disposed;

        #endregion

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }
            if (!disposing)
            {
                return;
            }

            // Clean up managed resources

            // Checks to see if we're being cancelled normally or abnormally.
            // If we're being forced closed, we should still try to cancel
            // our child tasks gracefully.
            if (!_cancellationTokenSource.Token.IsCancellationRequested)
            {
                _cancellationTokenSource.Cancel();
            }
            while (_listenTask != null && !_listenTask.IsCanceled && !_listenTask.IsCompleted && !_listenTask.IsFaulted)
            {
            }
            _cancellationTokenSource.Dispose();

            // All done
            _disposed = true;
        }

        ~VirtualComPort()
        {
            Dispose(false);
        }

        #endregion

        #region Private Fields

        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
        private readonly Task _listenTask;
        private readonly List<byte> _local = new List<byte>();
        private readonly SortedDictionary<int, byte[]> _responseDictionary = new SortedDictionary<int, byte[]>();
        private readonly Queue<byte> _serialQueue = new Queue<byte>();
        private bool _inSysex;

        #endregion
    }
}